StringBuilder StringBuffer
String부터 알아보기
StringBuilder
와 StringBuffer
는 모두 String 조작을 도와주는 클래스입니다.
간단한 +, - 같은 연산자로도 String 조작이 가능하지만 효율성이 좋지 않습니다.
그 이유는 String은 불변 객체이기 때문입니다. 왜 효율적인지 알기 위해 String이 어떻게 불변 객체로 관리 되는지부터 살펴보겠습니다.
String 관리
String도 primitive 타입이 아닌 클래스이기 때문에 당연히 primitive 타입으로 구성됩니다.
private final byte[] value;
기능이 더 많지만 String을 단순하게 보면 byte 배열입니다.
final
로 선언되어 생성 이후에 수정할 수 없는 것을 알 수 있습니다.
그렇다면 final
로 수정이 불가능한데 어떻게 이런 코드가 가능할까요?
String apple = "appl";
apple += "e";
이 코드는 Java compiler에 의해 밑의 코드와 비슷하게 변경됩니다.
String apple = "appl";
apple = new StringBuilder(apple).append("e").toString();
다시 말하면 String 연산은 내부적으로 StringBuidler(StringBuffer)
와 같은 구현을 사용합니다.
같은 구현을 사용하는데 왜 연산을 할 때 StringBuilder
가 더 효율적이라고 할까요?
String 메모리 관리
StringBuilder
가 더 효율적인 부분은 시간이 아닌 메모리에서 차이가 납니다.
그 이유를 알기 위해선 String이 어떻게 메모리에서 관리되는지 알아야 합니다.
String은 내부적으로 힙 메모리의 Constant Pool에서 관리됩니다.
"apple" 같은 리터럴 문자를 힙 메모리에서 공유해서 사용하게 됩니다.
String a = "apple";
String b = "apple";
assertEquals(a == b);
서로 다른 객체를 생성했더라도 같은 리터럴 문자열을 가리키면 주소값은 같게 됩니다.
여기서 "appl" 에서 "e"를 더했을 때 a의 주소값은 "apple"을 가리키는 b와 같아지게 됩니다. 계산해서 나온 "apple" 이라는 문자열이 Constant Pool에 올라가고 그 주소를 가리키게 되서입니다. 문제는 String은 불변 객체이기에 "appl"은 남아있다는 점입니다. "apple"은 StringBuilder에 의해서 새로 만들어진 문자열입니다. 따라서 Constant Pool 에는 "appl" 과 "apple" 둘 다 존재하게 됩니다.
String a = "appl";
String b = "apple";
a += "e";
assertEquals(a == b);
즉 String 자체로 연산을 하다보면 Constant Pool에 사용하지 않는 문자열들이 쌓이게 됩니다.
이렇게 참조되지 않는 문자열들은 이후에 Garbage Collector에 의해 비워집니다.
그렇다면 StringBuilder
는 어떻게 관리할까요?
StringBuilder 메모리 관리
StringBuilder
도 String 처럼 byte 배열로 이루어져있습니다.
하지만 차이점이라면 final이 없다는 점입니다.
즉 생성 이후에도 값이 변경될 수 있다는 걸 의미합니다.
byte[] value;
StringBuilder
에서 가장 핵심적인 기능은 insert와 append입니다.
둘 다 String을 추가하는 기능을 맡고 있습니다.
간단하게 생각하면 함수 인자로 들어온 문자열들을 받아와 원하는 배열 위치에 넣어줍니다.
하지만 배열이라 크기가 고정되어있기 때문에 크기가 더 커지는 경우 자동으로 배열을 새로 할당 받아 큰 배열에 문자열들을 복사해오는 기능을 맡습니다.
여기까지 정리해보면 StringBuilder
는 연산마다 Constant Pool에 사용되지 않는 문자열이 남지 않습니다.
사용하는 문자열만 객체의 byte 배열에서 관리하고 있기 때문에 메모리 영역에서 효율적입니다.
StringBuffer와의 차이
StringBuffer
와 StringBuilder
의 차이점은 Thread Safe입니다.
StringBuffer
와StringBuilder
에서 핵심 기능인 append로 비교해보겠습니다.
차이점은 toStringCache 라는 부분과 synchronized 입니다.
// StringBuilder
@Override
@HotSpotIntrinsicCandidate
public StringBuilder append(String str) {
super.append(str);
return this;
}
// StringBuffer
@Override
@HotSpotIntrinsicCandidate
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
멀티 쓰레드 환경에서 안전하기 위해서 같은 기능을 하는 메서드에 synchronzied 처리를 한 것 외에 크게 다른 게 없습니다. 실제로도 두 클래스는 모두 AbstractStringBuilder 클래스를 상속받고 있으며 super.append() 처리시 그 클래스에서 실제 append가 실행됩니다. 하지만 synchronzied 때문에 동기화 처리를 위한 시간이 더 소모되고, 싱글 스레드 환경에서도 기본 메서드보다 속도가 저하됩니다.
결론
-
싱글스레드 환경
StringBuilder
사용
-
멀티스레드 환경
- 지역 변수인 경우 :
StringBuilder
사용 - 인스턴스 변수, 클래스 변수인 경우 :
StringBuffer
사용
- 지역 변수인 경우 :